package isjinhao.jlox.run;

import isjinhao.jlox.Lox;
import isjinhao.jlox.grammar.expression.represention.*;
import isjinhao.jlox.grammar.expression.visitor.ExpressionVisitor;
import isjinhao.jlox.grammar.statement.represention.Assignment;
import isjinhao.jlox.grammar.statement.represention.Print;
import isjinhao.jlox.grammar.statement.represention.Statement;
import isjinhao.jlox.grammar.statement.represention.VarDefinition;
import isjinhao.jlox.grammar.statement.visitor.StatementVisitor;
import isjinhao.jlox.lexical.Token;

import java.util.List;

/**
 * @Author ISJINHAO
 * @Date 2021/2/21 19:49
 */
public class Interpreter implements ExpressionVisitor<Object>, StatementVisitor<Void> {

    private Environment environment = new Environment();

    @Override
    public Object visitLiteralExpr(Literal expr) {
        return expr.getValue();
    }

    @Override
    public Object visitGroupingExpr(Grouping expr) {
        return visit(expr.getExpression());
    }

    @Override
    public Object visitUnaryExpr(Unary expr) {
        Object right = visit(expr.getRight());

        switch (expr.getOperator().getType()) {
            case BANG:
                return !isTruthy(right);
            case MINUS:
                checkNumberOperand(expr.getOperator(), right);
                return -(double) right;
        }

        // Unreachable.
        return null;
    }

    private void checkNumberOperand(Token operator, Object operand) {
        if (operand instanceof Double) {
            return;
        }
        throw new RuntimeError(operator, "Operand must be a number.");
    }

    private boolean isTruthy(Object object) {
        if (object == null) {
            return false;
        }
        if (object instanceof Boolean) {
            return (boolean) object;
        }
        return true;
    }

    @Override
    public Object visitArithmeticExpr(Arithmetic expr) {
        Object left = visit(expr.getLeft());
        Object right = visit(expr.getRight());

        switch (expr.getOperator().getType()) {
            case BANG_EQUAL:
                return !isEqual(left, right);
            case EQUAL_EQUAL:
                return isEqual(left, right);
            case GREATER:
                checkNumberOperands(expr.getOperator(), left, right);
                return (double) left > (double) right;
            case GREATER_EQUAL:
                checkNumberOperands(expr.getOperator(), left, right);
                return (double) left >= (double) right;
            case LESS:
                checkNumberOperands(expr.getOperator(), left, right);
                return (double) left < (double) right;
            case LESS_EQUAL:
                checkNumberOperands(expr.getOperator(), left, right);
                return (double) left <= (double) right;
            case MINUS:
                checkNumberOperands(expr.getOperator(), left, right);
                return (double) left - (double) right;
            case PLUS:
                checkNumberOperands(expr.getOperator(), left, right);
                if (left instanceof Double && right instanceof Double) {
                    return (double) left + (double) right;
                }
                if (left instanceof String && right instanceof String) {
                    return (String) left + (String) right;
                }
                throw new RuntimeError(expr.getOperator(),
                        "Operands must be two numbers or two strings.");
            case SLASH:
                checkNumberOperands(expr.getOperator(), left, right);
                return (double) left / (double) right;
            case STAR:
                checkNumberOperands(expr.getOperator(), left, right);
                return (double) left * (double) right;
        }

        // Unreachable.
        return null;
    }

    private void checkNumberOperands(Token operator,
                                     Object left, Object right) {
        if (left instanceof Double && right instanceof Double) {
            return;
        }

        throw new RuntimeError(operator, "Operands must be numbers.");
    }

    private boolean isEqual(Object a, Object b) {
        if (a == null && b == null) {
            return true;
        }
        if (a == null) {
            return false;
        }
        return a.equals(b);
    }


    public void interpret(List<Statement> statements) {
        try {
            for (Statement statement : statements) {
                execute(statement);
            }
        } catch (RuntimeError error) {
            Lox.runtimeError(error);
        }
    }

    private void execute(Statement statement) {
        statement.accept(this);
    }

    private String stringify(Object object) {
        if (object == null) {
            return "nil";
        }
        if (object instanceof Double) {
            String text = object.toString();
            if (text.endsWith(".0")) {
                text = text.substring(0, text.length() - 2);
            }
            return text;
        }
        return object.toString();
    }

//    @Override
//    public Void visitExpressionStmt(ExpressionStatement stmt) {
//        evaluate(stmt.getExpression());
//        return null;
//    }

    @Override
    public Void visitPrintStmt(Print stmt) {
        Object value = visit(stmt.getExpression());
        System.out.println(stringify(value));
        return null;
    }

    @Override
    public Void visitVarDefinitionStmt(VarDefinition stmt) {
        Object value = null;
        if (stmt.getInitializer() != null) {
            value = visit(stmt.getInitializer());
        }

        environment.define(stmt.getName().getLexeme(), value);
        return null;
    }

    @Override
    public Object visitVariableExpr(Identifier expr) {
        return environment.get(expr.getName());
    }

    public Object visitAssignExpr(Assignment expr) {
        Object value = visit(expr.getValue());
        environment.assign(expr.getName(), value);
        return value;
    }

//    @Override
//    public Void visitBlockStmt(Block stmt) {
//        executeBlock(stmt.getStatements(), new Environment(environment));
//        return null;
//    }

    public void executeBlock(List<Statement> statements, Environment environment) {
        Environment previous = this.environment;
        try {
            this.environment = environment;
            for (Statement statement : statements) {
                execute(statement);
            }
        } finally {
            this.environment = previous;
        }
    }

}